package japgolly.scalajs.react.extra.router

import japgolly.scalajs.react.extra.internal.RouterMacros
import japgolly.scalajs.react.util.DefaultEffects.Sync
import japgolly.scalajs.react.util.Util.identityFn
import japgolly.scalajs.react.vdom.VdomElement
import java.util.UUID
import java.util.regex.{Matcher, Pattern}
import org.scalajs.dom.window.console
import scala.reflect.ClassTag
import scala.scalajs.js.URIUtils._
import scala.util.matching.Regex

/**
 * This is not meant to be imported by library-users;
 * [[RouterConfigDsl]] is the entire library-user-facing facade & DSL.
 */
object StaticDsl {

  /**
   * Route builder. Allows you to specify routes like `"user" / int / "display"`.
   * Once complete, [[RouteB]] will become a [[Route]].
   */
  object RouteB {
    trait Composition[A, B] {
      type C
      val ga: C => A
      val gb: C => B
      val gc: (A, B) => C
      def apply(fa: RouteB[A], fb: RouteB[B]): RouteB[C] =
        new RouteB(
          fa.regex + fb.regex,
          fa.matchGroups + fb.matchGroups,
          g => for {a <- fa.parse(g); b <- fb.parse(i => g(i + fa.matchGroups))} yield gc(a, b),
          c => fa.build(ga(c)) + fb.build(gb(c)))
    }

    trait Composition_PriLowest {
      implicit def ***[A, B]: Composition.Aux[A, B, (A, B)]= Composition(_._1, _._2, (_, _))
    }
    trait Composition_PriLow extends Composition_PriLowest {
      // Generated by bin/gen-router
      implicit def T3[A,B,C]: Composition.Aux[(A,B), C, (A,B,C)] = Composition(r => (r._1,r._2), _._3, (l,r) => (l._1,l._2,r))
      implicit def T4[A,B,C,D]: Composition.Aux[(A,B,C), D, (A,B,C,D)] = Composition(r => (r._1,r._2,r._3), _._4, (l,r) => (l._1,l._2,l._3,r))
      implicit def T5[A,B,C,D,E]: Composition.Aux[(A,B,C,D), E, (A,B,C,D,E)] = Composition(r => (r._1,r._2,r._3,r._4), _._5, (l,r) => (l._1,l._2,l._3,l._4,r))
      implicit def T6[A,B,C,D,E,F]: Composition.Aux[(A,B,C,D,E), F, (A,B,C,D,E,F)] = Composition(r => (r._1,r._2,r._3,r._4,r._5), _._6, (l,r) => (l._1,l._2,l._3,l._4,l._5,r))
      implicit def T7[A,B,C,D,E,F,G]: Composition.Aux[(A,B,C,D,E,F), G, (A,B,C,D,E,F,G)] = Composition(r => (r._1,r._2,r._3,r._4,r._5,r._6), _._7, (l,r) => (l._1,l._2,l._3,l._4,l._5,l._6,r))
      implicit def T8[A,B,C,D,E,F,G,H]: Composition.Aux[(A,B,C,D,E,F,G), H, (A,B,C,D,E,F,G,H)] = Composition(r => (r._1,r._2,r._3,r._4,r._5,r._6,r._7), _._8, (l,r) => (l._1,l._2,l._3,l._4,l._5,l._6,l._7,r))
      implicit def T9[A,B,C,D,E,F,G,H,I]: Composition.Aux[(A,B,C,D,E,F,G,H), I, (A,B,C,D,E,F,G,H,I)] = Composition(r => (r._1,r._2,r._3,r._4,r._5,r._6,r._7,r._8), _._9, (l,r) => (l._1,l._2,l._3,l._4,l._5,l._6,l._7,l._8,r))
      implicit def T10[A,B,C,D,E,F,G,H,I,J]: Composition.Aux[(A,B,C,D,E,F,G,H,I), J, (A,B,C,D,E,F,G,H,I,J)] = Composition(r => (r._1,r._2,r._3,r._4,r._5,r._6,r._7,r._8,r._9), _._10, (l,r) => (l._1,l._2,l._3,l._4,l._5,l._6,l._7,l._8,l._9,r))
      implicit def T11[A,B,C,D,E,F,G,H,I,J,K]: Composition.Aux[(A,B,C,D,E,F,G,H,I,J), K, (A,B,C,D,E,F,G,H,I,J,K)] = Composition(r => (r._1,r._2,r._3,r._4,r._5,r._6,r._7,r._8,r._9,r._10), _._11, (l,r) => (l._1,l._2,l._3,l._4,l._5,l._6,l._7,l._8,l._9,l._10,r))
      implicit def T12[A,B,C,D,E,F,G,H,I,J,K,L]: Composition.Aux[(A,B,C,D,E,F,G,H,I,J,K), L, (A,B,C,D,E,F,G,H,I,J,K,L)] = Composition(r => (r._1,r._2,r._3,r._4,r._5,r._6,r._7,r._8,r._9,r._10,r._11), _._12, (l,r) => (l._1,l._2,l._3,l._4,l._5,l._6,l._7,l._8,l._9,l._10,l._11,r))
      implicit def T13[A,B,C,D,E,F,G,H,I,J,K,L,M]: Composition.Aux[(A,B,C,D,E,F,G,H,I,J,K,L), M, (A,B,C,D,E,F,G,H,I,J,K,L,M)] = Composition(r => (r._1,r._2,r._3,r._4,r._5,r._6,r._7,r._8,r._9,r._10,r._11,r._12), _._13, (l,r) => (l._1,l._2,l._3,l._4,l._5,l._6,l._7,l._8,l._9,l._10,l._11,l._12,r))
      implicit def T14[A,B,C,D,E,F,G,H,I,J,K,L,M,N]: Composition.Aux[(A,B,C,D,E,F,G,H,I,J,K,L,M), N, (A,B,C,D,E,F,G,H,I,J,K,L,M,N)] = Composition(r => (r._1,r._2,r._3,r._4,r._5,r._6,r._7,r._8,r._9,r._10,r._11,r._12,r._13), _._14, (l,r) => (l._1,l._2,l._3,l._4,l._5,l._6,l._7,l._8,l._9,l._10,l._11,l._12,l._13,r))
      implicit def T15[A,B,C,D,E,F,G,H,I,J,K,L,M,N,O]: Composition.Aux[(A,B,C,D,E,F,G,H,I,J,K,L,M,N), O, (A,B,C,D,E,F,G,H,I,J,K,L,M,N,O)] = Composition(r => (r._1,r._2,r._3,r._4,r._5,r._6,r._7,r._8,r._9,r._10,r._11,r._12,r._13,r._14), _._15, (l,r) => (l._1,l._2,l._3,l._4,l._5,l._6,l._7,l._8,l._9,l._10,l._11,l._12,l._13,l._14,r))
      implicit def T16[A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P]: Composition.Aux[(A,B,C,D,E,F,G,H,I,J,K,L,M,N,O), P, (A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P)] = Composition(r => (r._1,r._2,r._3,r._4,r._5,r._6,r._7,r._8,r._9,r._10,r._11,r._12,r._13,r._14,r._15), _._16, (l,r) => (l._1,l._2,l._3,l._4,l._5,l._6,l._7,l._8,l._9,l._10,l._11,l._12,l._13,l._14,l._15,r))
      implicit def T17[A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q]: Composition.Aux[(A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P), Q, (A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q)] = Composition(r => (r._1,r._2,r._3,r._4,r._5,r._6,r._7,r._8,r._9,r._10,r._11,r._12,r._13,r._14,r._15,r._16), _._17, (l,r) => (l._1,l._2,l._3,l._4,l._5,l._6,l._7,l._8,l._9,l._10,l._11,l._12,l._13,l._14,l._15,l._16,r))
      implicit def T18[A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R]: Composition.Aux[(A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q), R, (A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R)] = Composition(r => (r._1,r._2,r._3,r._4,r._5,r._6,r._7,r._8,r._9,r._10,r._11,r._12,r._13,r._14,r._15,r._16,r._17), _._18, (l,r) => (l._1,l._2,l._3,l._4,l._5,l._6,l._7,l._8,l._9,l._10,l._11,l._12,l._13,l._14,l._15,l._16,l._17,r))
      implicit def T19[A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S]: Composition.Aux[(A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R), S, (A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S)] = Composition(r => (r._1,r._2,r._3,r._4,r._5,r._6,r._7,r._8,r._9,r._10,r._11,r._12,r._13,r._14,r._15,r._16,r._17,r._18), _._19, (l,r) => (l._1,l._2,l._3,l._4,l._5,l._6,l._7,l._8,l._9,l._10,l._11,l._12,l._13,l._14,l._15,l._16,l._17,l._18,r))
      implicit def T20[A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T]: Composition.Aux[(A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S), T, (A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T)] = Composition(r => (r._1,r._2,r._3,r._4,r._5,r._6,r._7,r._8,r._9,r._10,r._11,r._12,r._13,r._14,r._15,r._16,r._17,r._18,r._19), _._20, (l,r) => (l._1,l._2,l._3,l._4,l._5,l._6,l._7,l._8,l._9,l._10,l._11,l._12,l._13,l._14,l._15,l._16,l._17,l._18,l._19,r))
      implicit def T21[A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U]: Composition.Aux[(A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T), U, (A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U)] = Composition(r => (r._1,r._2,r._3,r._4,r._5,r._6,r._7,r._8,r._9,r._10,r._11,r._12,r._13,r._14,r._15,r._16,r._17,r._18,r._19,r._20), _._21, (l,r) => (l._1,l._2,l._3,l._4,l._5,l._6,l._7,l._8,l._9,l._10,l._11,l._12,l._13,l._14,l._15,l._16,l._17,l._18,l._19,l._20,r))
      implicit def T22[A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V]: Composition.Aux[(A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U), V, (A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V)] = Composition(r => (r._1,r._2,r._3,r._4,r._5,r._6,r._7,r._8,r._9,r._10,r._11,r._12,r._13,r._14,r._15,r._16,r._17,r._18,r._19,r._20,r._21), _._22, (l,r) => (l._1,l._2,l._3,l._4,l._5,l._6,l._7,l._8,l._9,l._10,l._11,l._12,l._13,l._14,l._15,l._16,l._17,l._18,l._19,l._20,l._21,r))
    }
    trait Composition_PriMed extends Composition_PriLow {
      implicit def _toA[A]: Composition.Aux[Unit, A, A] = Composition(_ => (), identityFn, (_, a) => a)
      implicit def Ato_[A]: Composition.Aux[A, Unit, A] = Composition(identityFn, _ => (), (a, _) => a)
    }
    object Composition extends Composition_PriMed {
      implicit def _to_ : Composition.Aux[Unit, Unit, Unit] = Composition(_ => (), _ => (), (_, _) => ())

      type Aux[A, B, O] = Composition[A, B] {type C = O}

      def apply[A, B, O](a: O => A, b: O => B, c: (A, B) => O): Aux[A, B, O] =
        new Composition[A, B] {
          override type C = O
          val ga = a
          val gb = b
          val gc = c
        }
    }

    private val someUnit = Some(())

    def literal(s: String): RouteB[Unit] =
      new RouteB(Pattern.quote(s), 0, _ => someUnit, _ => s)

    val / = literal("/")
  }

  abstract class RouteCommon[R[X] <: RouteCommon[R, X], A] {

    def parseThen(f: Option[A] => Option[A]): R[A]

    /**
     * Prism map.
     *
     * Some values of `A` can be turned into a `B`s, some fail (in which case the route is considered non-matching).
     *
     * All `B`s can be turned back into `A`s.
     */
    def pmap[B](b: A => Option[B])(a: B => A): R[B]

    /**
     * Exponential map.
     *
     * Any `A` can be turned into a `B` and vice versa.
     */
    final def xmap[B](b: A => B)(a: B => A): R[B] =
      pmap(a => Some(b(a)))(a)

    final def filter(f: A => Boolean): R[A] =
      parseThen(_ filter f)

    final def mapParsed[B <: A](f: A => B): R[B] =
      xmap(f)(x => x)

    final def mapInput[B >: A](f: B => A): R[B] =
      xmap[B](x => x)(f)

    import japgolly.scalajs.react.internal.ValueOfCompat.ValueOf
    final def const[B](b: B)(implicit ev: ValueOf[A]): R[B] =
      xmap(_ => b)(_ => ev.value)
  }

  /**
   * A fragment of a route. Can be composed with other fragments.
   *
   * @param matchGroups The number of matches that `regex` will capture.
   */
  class RouteB[A](val regex: String,
                  val matchGroups: Int,
                  val parse: (Int => String) => Option[A],
                  val build: A => String) extends RouteCommon[RouteB, A] with RouterMacros.ForRouteB[A] {
    import RouteB.Composition

    override def toString =
      s"RouteB($regex)"

    def ~[B](next: RouteB[B])(implicit c: Composition[A, B]): RouteB[c.C] =
      c(this, next)

    def /[B](next: RouteB[B])(implicit c: Composition[A, B]): RouteB[c.C] =
      this ~ RouteB./ ~ next

    override def parseThen(f: Option[A] => Option[A]): RouteB[A] =
      new RouteB(regex, matchGroups, f compose parse, build)

    override def pmap[B](b: A => Option[B])(a: B => A): RouteB[B] =
      new RouteB(regex, matchGroups, parse(_) flatMap b, build compose a)

    def option: RouteB[Option[A]] =
      new RouteB[Option[A]](s"($regex)?", matchGroups + 1,
        g => Some(if (g(0) eq null) None else parse(i => g(i + 1))),
        _.fold("")(build))

    final def route: Route[A] = {
      val p = Pattern.compile("^" + regex + "$")
      // https://github.com/scala-js/scala-js/issues/1727
      // val g = p.matcher("").groupCount
      // if (g != matchGroups)
      //   sys.error(s"Error in regex: /${p.pattern}/. Expected $matchGroups match groups but detected $g.")
      new Route(p, m => parse(i => m.group(i + 1)), a => Path(build(a)))
    }
  }

  class RouteBO[A](private val r: RouteB[Option[A]]) extends AnyVal {

    /**
     * Specify a default value when parsing.
     *
     * Note: Unlike [[withDefault()]] path generation will still explicitly include the default value.
     *
     * Eg. If the path is like "/file[.format]" and the default is JSON, "/file" will be read as "/file.json", but
     * when generating a path with JSON this will generate "/file.json" instead of "/file".
     */
    def parseDefault(default: => A): RouteB[A] =
      r.xmap(_ getOrElse default)(Some(_))

    /**
     * Specify a default value.
     *
     * Note: Unlike [[parseDefault()]] this will affect path generation too.
     *
     * Eg. If the path is like "/file[.format]" and the default is JSON, "/file" will be read as "/file.json", and
     * when generating a path with JSON this will generate "/file" instead of "/file.json".
     *
     * Make sure the type has a useful `.equals()` implementation.
     * Example: `default == default` should be `true`.
     */
    def withDefault(default: => A): RouteB[A] =
      r.xmap(_ getOrElse default)(a => if (default == a) None else Some(a))
  }

  /**
   * A complete route.
   */
  final class Route[A](pattern: Pattern,
                       parseFn: Matcher => Option[A],
                       buildFn: A => Path) extends RouteCommon[Route, A] with RouterMacros.ForRoute[A] {
    override def toString =
      s"Route($pattern)"

    override def parseThen(f: Option[A] => Option[A]): Route[A] =
      new Route(pattern, f compose parseFn, buildFn)

    override def pmap[B](b: A => Option[B])(a: B => A): Route[B] =
      new Route(pattern, parseFn(_) flatMap b, buildFn compose a)

    def parse(path: Path): Option[A] = {
      val m = pattern.matcher(path.value)
      if (m.matches)
        parseFn(m)
      else
        None
    }

    def pathFor(a: A): Path =
      buildFn(a)
  }

  // ===================================================================================================================

  final class DynamicRouteB[Page, Props, P <: Page, O](private val f: (P => Action[Page, Props]) => O) extends AnyVal {
    def ~>(g: P => Action[Page, Props]): O = f(g)
  }

  final class StaticRouteB[Page, Props, O](private val f: (=> Action[Page, Props]) => O) extends AnyVal {
    def ~>(a: => Action[Page, Props]): O = f(a)
  }

  final class StaticRedirectB[Page, Props, O](private val f: (=> Redirect[Page]) => O) extends AnyVal {
    def ~>(a: => Redirect[Page]): O = f(a)
  }

  final class DynamicRedirectB[Page, Props, A, O](private val f: (A => Redirect[Page]) => O) extends AnyVal {
    def ~>(a: A => Redirect[Page]): O = f(a)
  }
}

// =====================================================================================================================
// =====================================================================================================================

object RouterConfigDsl {
  def apply[Page] =
    new BuildInterface[Page, Unit]

  class BuildInterface[Page, Props] {
    def use[A](f: RouterConfigDsl[Page, Props] => A): A =
      f(new RouterConfigDsl)

    def buildConfig(f: RouterConfigDsl[Page, Props] => RouterWithPropsConfig[Page, Props]): RouterWithPropsConfig[Page, Props] =
      use(f)

    def buildRule(f: RouterConfigDsl[Page, Props] => RoutingRule[Page, Props]): RoutingRule[Page, Props] =
      use(f)
  }
}

object RouterWithPropsConfigDsl {
  def apply[Page, Props] =
    new RouterConfigDsl.BuildInterface[Page, Props]
}

/**
 * DSL for creating [[RouterConfig]].
 *
 * Instead creating an instance of this yourself, use [[RouterConfigDsl.apply]].
 */
final class RouterConfigDsl[Page, Props] {
  import StaticDsl._

  type Action   = japgolly.scalajs.react.extra.router.Action[Page, Props]
  type Renderer = japgolly.scalajs.react.extra.router.Renderer[Page, Props]
  type Redirect = japgolly.scalajs.react.extra.router.Redirect[Page]
  type Parsed   = RouterConfig.Parsed[Page]

  // -------------------------------------------------------------------------------------------------------------------
  // Route DSL

  private def uuidRegex = "([A-Fa-f0-9]{8}(?:-[A-Fa-f0-9]{4}){3}-[A-Fa-f0-9]{12})"

  def root = Path.root
  val int  = new RouteB[Int] ("(-?\\d+)", 1, g => Some(g(0).toInt),           _.toString)
  val long = new RouteB[Long]("(-?\\d+)", 1, g => Some(g(0).toLong),          _.toString)
  val uuid = new RouteB[UUID](uuidRegex,  1, g => Some(UUID fromString g(0)), _.toString)

  private def __string1(regex: String): RouteB[String] =
    new RouteB(regex, 1, g => Some(g(0)), identityFn)

  /**
   * Matches a string.
   *
   * Best to use a whitelist of characters, eg. "[a-zA-Z0-9]+".
   * Do not capture groups; use "[a-z]+" instead of "([a-z]+)".
   * If you need to group, use non-capturing groups like "(?:bye|hello)" instead of "(bye|hello)".
   */
  def string(regex: String): RouteB[String] =
    __string1("(" + regex + ")")

  /** Captures the (non-empty) remaining portion of the URL path. */
  def remainingPath: RouteB[String] =
    __string1("(.+)$")

  /** Captures the (potentially-empty) remaining portion of the URL path. */
  def remainingPathOrBlank: RouteB[String] =
    __string1("(.*)$")

  implicit def _ops_for_routeb_option[A](r: RouteB[Option[A]]): RouteBO[A] = new RouteBO(r)

  implicit def _auto_routeB_from_str(l: String): RouteB[Unit] = RouteB.literal(l)
  implicit def _auto_routeB_from_path(p: Path): RouteB[Unit] = RouteB.literal(p.value)
  implicit def _auto_route_from_routeB[A, R](r: R)(implicit ev: R => RouteB[A]): Route[A] = ev(r).route

  // -------------------------------------------------------------------------------------------------------------------
  // Action DSL

  implicit def _auto_someAction[A <: Action](a: A): Option[A] = Some(a)

  def render[A](a: => A)(implicit ev: A => VdomElement): Renderer =
    RendererF(_ => _ => ev(a))

  def renderR[A](g: RouterCtl[Page] => A)(implicit ev: A => VdomElement): Renderer =
    RendererF(r => _ => ev(g(r)))

  def renderP[A](g: Props => A)(implicit ev: A => VdomElement): Renderer =
    RendererF(_ => props => ev(g(props)))

  def renderRP[A](g: (RouterCtl[Page], Props) => A)(implicit ev: A => VdomElement): Renderer =
    RendererF(r => props => ev(g(r, props)))

  def dynRender[P <: Page, A](g: P => A)(implicit ev: A => VdomElement): P => Renderer =
    p => RendererF(_ => _ => ev(g(p)))

  def dynRenderR[P <: Page, A](g: (P, RouterCtl[Page]) => A)(implicit ev: A => VdomElement): P => Renderer =
    p => RendererF(r => _ => ev(g(p, r)))

  def dynRenderP[P <: Page, A](g: (P, Props) => A)(implicit ev: A => VdomElement): P => Renderer =
    p => RendererF(_ => props => ev(g(p, props)))

  def dynRenderRP[P <: Page, A](g: (P, RouterCtl[Page], Props) => A)(implicit ev: A => VdomElement): P => Renderer =
    p => RendererF(r => props => ev(g(p, r, props)))

  def redirectToPage(page: Page)(implicit via: SetRouteVia): RedirectToPage[Page] =
    RedirectToPage[Page](page, via)

  def redirectToPath(path: Path)(implicit via: SetRouteVia): RedirectToPath[Page] =
    RedirectToPath[Page](path, via)

  def redirectToPath(path: String)(implicit via: SetRouteVia): RedirectToPath[Page] =
    redirectToPath(Path(path))

  // -------------------------------------------------------------------------------------------------------------------
  // Rule building DSL

  type Rule = japgolly.scalajs.react.extra.router.RoutingRule[Page, Props]
  type Rules = japgolly.scalajs.react.extra.router.RoutingRule.WithFallback[Page, Props]
  def emptyRule: Rule = RoutingRule.empty

  implicit def _auto_parsed_from_redirect(r: Redirect): Parsed = Left(r)
  implicit def _auto_parsed_from_page    (p: Page)    : Parsed = Right(p)

  implicit def _auto_parsedO_from_parsed [A](p: A)        (implicit ev: A => Parsed): Option[Parsed] = Some(ev(p))
  implicit def _auto_parsedO_from_parsedO[A](o: Option[A])(implicit ev: A => Parsed): Option[Parsed] = o.map(ev)

  implicit def _auto_notFound_from_parsed [A](a: A)        (implicit ev: A => Parsed): Path => Parsed = _ => ev(a)
  implicit def _auto_notFound_from_parsedF[A](f: Path => A)(implicit ev: A => Parsed): Path => Parsed = p => ev(f(p))

  implicit def _auto_routeParser_from_parsed  [A](a: A)                (implicit ev: A => Parsed): Path => Option[Parsed] = _ => Some(ev(a))
  implicit def _auto_routeParser_from_parsedF [A](f: Path => A)        (implicit ev: A => Parsed): Path => Option[Parsed] = p => Some(ev(f(p)))
  implicit def _auto_routeParser_from_parsedO [A](o: Option[A])        (implicit ev: A => Parsed): Path => Option[Parsed] = _ => o.map(ev)
  implicit def _auto_routeParser_from_parsedFO[A](f: Path => Option[A])(implicit ev: A => Parsed): Path => Option[Parsed] = f(_).map(ev)

  // allows dynamicRoute ~~> X to not care if X is (Action) or (P => Action)
  implicit def _auto_pToAction_from_action(a: => Action): Page => Action = _ => a

  implicit def _auto_rules_from_rulesB(r: Rule): Rules = r.noFallback

  // Only really aids rewriteRuleR but safe anyway
  implicit def _auto_pattern_from_regex(r: Regex): Pattern = r.pattern

  /**
   * Note: Requires that `Page#equals()` be sensible.
   */
  def staticRoute(r: Route[Unit], page: Page): StaticRouteB[Page, Props, Rule] = {
    val dyn = dynamicRoute(r const page){ case p if page == p => p }
    new StaticRouteB(a => dyn ~> a)
  }

  def dynamicRoute[P <: Page](r: Route[P])(pf: PartialFunction[Page, P]): DynamicRouteB[Page, Props, P, Rule] =
    dynamicRouteF(r)(pf.lift)

  def dynamicRouteF[P <: Page](r: Route[P])(op: Page => Option[P]): DynamicRouteB[Page, Props, P, Rule] = {
    def onPage[A](f: P => A)(page: Page): Option[A] =
      op(page) map f
    new DynamicRouteB(a => RoutingRule.Atom(r.parse, onPage(r.pathFor), (_, p) => onPage(a)(p)))
  }

  def dynamicRouteCT[P <: Page](r: Route[P])(implicit ct: ClassTag[P]): DynamicRouteB[Page, Props, P, Rule] =
    dynamicRouteF(r)(ct.unapply)

  def staticRedirect(r: Route[Unit]): StaticRedirectB[Page, Props, Rule] =
    new StaticRedirectB(a => rewritePathF(r.parse(_) map (_ => a)))

  def dynamicRedirect[A](r: Route[A]): DynamicRedirectB[Page, Props, A, Rule] =
    new DynamicRedirectB(f => rewritePathF(r.parse(_) map f))

  def rewritePath(pf: PartialFunction[Path, Redirect]): Rule =
    rewritePathF(pf.lift)

  def rewritePathF(f: Path => Option[Redirect]): Rule =
    RoutingRule parseOnly f

  def rewritePathR(r: Pattern, f: Matcher => Option[Redirect]): Rule =
    rewritePathF { p =>
      val m = r.matcher(p.value)
      if (m.matches) f(m) else None
    }

  /** Captures the query portion of the URL.
    *
    * Note that this is not a strict capture, URLs without a query string will still be accepted,
    * and the parameter map will simply by empty.
    */
  lazy val queryToSeq: RouteB[Seq[(String, String)]] = {
    type A = Seq[(String, String)]

    val queryRegex = """(\?[^#]*)?"""

    val kvRegex = "^([^=]+)(?:=(.*))?$".r

    def decode(str: String): String =
      decodeURIComponent(str.replace('+', ' '))

    val needingEncoding = """[~!'()]|%[02]0""".r

    def encode(str: String): String =
      needingEncoding.replaceAllIn(encodeURIComponent(str), m =>
        m.group(0) match {
          case "%20" => "+"
          case "!"   => "%21"
          case "'"   => "%27"
          case "("   => "%28"
          case ")"   => "%29"
          case "~"   => "%7E"
          case "%00" => (0: Char).toString
        }
      )

    val parse: (Int => String) => Option[A] =
      _(0) match {
        case null => Some(Seq.empty)
        case q => Some(
          q
            .tail
            .split("&")
            .iterator
            .filter(_.nonEmpty)
            .map {
              case kvRegex(k, null) => decode(k) -> ""
              case kvRegex(k, v)    => decode(k) -> decode(v)
              case x =>
                console.warn(s"Unable to parse query string pair: $x")
                null
            }
            .filter(_ != null)
            .toSeq
        )
      }

    val build: A => String =
      a =>
        if (a.isEmpty)
          ""
        else
          a.iterator
            .map {
              case (k, "") => encode(k)
              case (k, v)  => s"${encode(k)}=${encode(v)}"
            }
            .mkString("?", "&", "")

    new RouteB[A](queryRegex, 1, parse, build)
  }

  /** Captures the query portion of the URL to a param map.
    *
    * Note that this is not a strict capture, URLs without a query string will still be accepted,
    * and the parameter map will simply by empty.
    */
  lazy val queryToMap: RouteB[Map[String, String]] =
    queryToSeq.xmap(_.toMap)(_.toSeq)

  /** Captures the query portion of the URL to a param multimap.
    *
    * Note that this is not a strict capture, URLs without a query string will still be accepted,
    * and the parameter map will simply by empty.
    */
  lazy val queryToMultimap: RouteB[Map[String, Seq[String]]] = {
    type S = Seq[(String, String)]
    type M = Map[String, Seq[String]]

    val to: S => M =
      _.foldLeft[M](Map.empty) {
        case (m, (k, v)) =>
          val newVs: Seq[String] =
            m.get(k) match {
              case None     => v :: Nil
              case Some(vs) => vs :+ v
            }
          m.updated(k, newVs)
      }

    val from: M => S =
      _.iterator.flatMap(x => x._2.iterator.map((x._1, _))).toSeq

    queryToSeq.xmap(to)(from)
  }

  // -------------------------------------------------------------------------------------------------------------------
  // Utilities

  /**
    * Removes the query portion of the URL.
    *
    * e.g. `a/b?c=1` to `a/b`
    */
  def removeQuery: Rule =
    rewritePathR("^(.*?)\\?.*$".r, m => redirectToPath(m group 1)(SetRouteVia.HistoryReplace))

  /**
   * A rule that uses a replace-state redirect to remove trailing slashes from route URLs.
   */
  def removeTrailingSlashes: Rule =
    rewritePathR("^(.*?)/+$".r, m => redirectToPath(m group 1)(SetRouteVia.HistoryReplace))

  /**
   * A rule that uses a replace-state redirect to remove leading slashes from route URLs.
   */
  def removeLeadingSlashes: Rule =
    rewritePathR("^/+(.*)$".r, m => redirectToPath(m group 1)(SetRouteVia.HistoryReplace))

  /**
   * A rule that uses a replace-state redirect to remove leading and trailing slashes from route URLs.
   */
  def trimSlashes: Rule = (
    rewritePathR("^/*(.*?)/+$".r, m => redirectToPath(m group 1)(SetRouteVia.HistoryReplace))
    | removeLeadingSlashes)
}
